Add short URL sharing via paste service to fix URL truncation#188
Add short URL sharing via paste service to fix URL truncation#188GunitBindal wants to merge 4 commits intobacknotprop:mainfrom
Conversation
…k/messaging apps Share URLs for large plans can be 10-40KB+ because the entire plan + annotations are compressed into the URL hash. Services like Slack, WhatsApp, and Twitter truncate these URLs, making shared plans unviewable (related: backnotprop#142). This adds an optional paste-service backend that stores compressed payloads and returns short ~60-char URLs (e.g. share.plannotator.ai/p/aBcDeFgH). Changes: - Add Cloudflare Worker paste service (apps/paste-worker/) with KV storage and 90-day TTL for stored plans - Add createShortShareUrl() and loadFromPasteId() to sharing utils - Update useSharing hook to auto-generate short URLs with 1s debounce - Update ExportModal to show short URL as primary copy target with full hash URL as backup - Portal automatically supports /p/<id> paths via useSharing hook - Fully backward compatible: hash-based URLs continue to work unchanged - Graceful degradation: falls back to hash URLs if paste service is unavailable
- Worker: return only { id } so client constructs URL with its own
shareBaseUrl (fixes self-hosted deployments)
- importFromShareUrl: handle /p/<id> short URLs in addition to hash
URLs (fixes teammate import via short links)
- Anchor /p/<id> regex with ^ to prevent false matches on nested paths
- Pass shareBaseUrl to loadFromPasteId (was always defaulting)
- replaceState preserves base path instead of hardcoding /
- Clear stale shortShareUrl immediately on debounce to prevent showing
outdated link during the 1s delay
- Add shareBaseUrl to dependency arrays for loadFromHash and
importFromShareUrl callbacks
- Remove unused url field fallback from paste API response parsing
|
Thank you for this @GunitBindal Am I right in assuming that this is primarily intended for self-hosters? Also, do you self-host? Right now, the static site can be somewhat trusted because it's still open source and I'm not logging anything. There's no backend. This sorta changes the nature if I support this, but I understand why it's a great value add for the self-hoster especially. It doesn't mean I'm not opposed to adding it for the |
|
Hey Michael — thanks for the quick response! Also messaged you on X about this. To answer your question: no, I don't self-host — I use So while the PR is designed to also work for self-hosters, the primary motivation is fixing sharing on I totally understand the trust concern — the beauty of the current design is zero backend. A few ways to keep that trust while enabling short URLs:
I really love Plannotator — it's become essential to my Claude Code workflow, and this is the one thing blocking me from sharing reviews with my team daily. I'm happy to implement any of the above approaches if you point me in a direction. Also have a few more feature ideas — if you're open to receiving PRs, I'd love to contribute more. |
|
These are all great options - I think for large plans we can just be transparent that there will be a temporary storage of the plan to enable sharing on Plannotator.ai. I'll add this as I review the PR today. Your feature idea for remote sessions is good - happy to take contributions it's also something I've been wanting as I do remote sessions now too. |
Code reviewFound 1 issue:
plannotator/packages/ui/hooks/useSharing.ts Lines 116 to 118 in 0e32665 plannotator/packages/ui/hooks/useSharing.ts Lines 274 to 276 in 0e32665 plannotator/packages/ui/utils/sharing.ts Lines 299 to 306 in 0e32665 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
Bug fix (addresses @backnotprop's code review): - useSharing hook now accepts separate `pasteApiUrl` parameter instead of incorrectly passing `shareBaseUrl` to `loadFromPasteId`. These are different domains (share portal vs paste backend). When omitted, the default `https://paste.plannotator.ai` is used correctly. - Fixed in all three call sites: loadFromHash, importFromShareUrl, and generateShortUrl (via createShortShareUrl options). Remote session share URLs (implements backnotprop#192): - When running on a remote instance (SSH, devcontainer), Plannotator now generates a share.plannotator.ai URL and prints it to stderr so the user can open the plan review in their local browser. - Works for all three modes: plan review, code review, and annotate. - Uses the same deflate-raw + base64url encoding as the client. - Fails silently if URL generation fails — port forwarding still works. - New server utility: packages/server/share-url.ts
|
Great catch @backnotprop — you're absolutely right. Fixed in the latest commit ( The bug fix:
Also in this commit — remote session support (#192): On the privacy/transparency topic: PrivateBin approach — instead of a custom Cloudflare Worker, use the PrivateBin protocol (which multiple free public instances support). The client encrypts with AES-256-GCM before uploading, and the key lives only in the URL fragment. The server provably cannot read the content — same zero-knowledge model as Excalidraw+. The share URL would be ~120 chars. Public instances have CORS enabled, no auth needed. That said, your approach of "just be transparent about temporary storage" is probably simpler and more practical for users. The Cloudflare Worker in this PR stores opaque compressed data with a 90-day TTL and the code is fully auditable in |
- share-url.ts: Replace btoa(String.fromCharCode(...compressed)) with a loop to avoid RangeError on plans >65K compressed bytes - share-url.ts: Add cross-reference comment noting the server-side SharePayload is an intentional subset of the canonical UI type - paste-worker: Only return CORS headers for allowed origins; disallowed origins get no CORS headers instead of misleading partial headers - useSharing.ts: Remove unused shareBaseUrl from loadFromHash dependency array (loadFromHash only uses pasteApiUrl, not shareBaseUrl)
|
Oh sorry for that comment hopefully wasn't distracting. I'm also evaluating my infra options bc I tend to deploy to AWS. Will consider Cloudflare here though. And behind the scenes I have a OpenClaw prototype for Plannotator I need to make some decisions for because it also requires a backend. Thank you for the continued effort! Excited to get this in. I plan to finalize and merge before weekend ends |
Thanks for a quick reply; I came up with a new PrivateBin approach as well. That might be also a good approach. What do you think? |
Summary
Fixes #187 — Share URLs for large plans are 10-40KB+ and get truncated by Slack, WhatsApp, email clients, etc., making the team sharing feature unusable for real-world plans. Also addresses the root cause behind #142 (silent fallback to demo plan on truncated URLs).
This adds an optional paste-service backend that stores compressed plan payloads and returns short ~50-char URLs like
share.plannotator.ai/p/aBcDeFgH.What changed
New:
apps/paste-worker/— Cloudflare Worker paste servicePOST /api/pastestores compressed plan data in KV, returns short IDGET /api/paste/:idretrieves stored dataModified:
packages/ui/utils/sharing.tscreateShortShareUrl()— POSTs compressed data to paste service (5s timeout)loadFromPasteId()— fetches plan by paste ID (10s timeout)toShareableImages(was module-private, now needed bycreateShortShareUrl)Modified:
packages/ui/hooks/useSharing.tsshortShareUrl,isGeneratingShortUrl,shortUrlErrorto hook stateloadFromHash()now checks for/p/<id>path pattern first, then falls back to hashModified:
packages/ui/components/ExportModal.tsxModified:
packages/editor/App.tsxuseSharingtoExportModalDesign decisions
PLANNOTATOR_PASTE_URLenv var overrides the paste API URLuseSharinghook detects/p/<id>paths, so the portal inherits short URL loading without any portal-specific changesDeployment note
The paste worker (
apps/paste-worker/) needs to be deployed separately topaste.plannotator.aiwith a Cloudflare KV namespace. Until deployed, the feature degrades gracefully — users see only the existing hash-based URL.Test plan
/p/<id>paths/#<hash>paths (backward compat)